package org.dru.clay.respository.transport.ssh;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;

import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.Buffer.BufferException;
import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.connection.channel.direct.Session.Command;
import net.schmizz.sshj.userauth.method.AuthMethod;
import net.schmizz.sshj.xfer.InMemoryDestFile;

import org.dru.clay.respository.transport.FileInfo;
import org.dru.clay.respository.transport.Transport;

import com.jcraft.jsch.agentproxy.AgentProxy;
import com.jcraft.jsch.agentproxy.Connector;
import com.jcraft.jsch.agentproxy.ConnectorFactory;
import com.jcraft.jsch.agentproxy.Identity;
import com.jcraft.jsch.agentproxy.sshj.AuthAgent;

public class SshTransport implements Transport {
	private final SSHClient ssh;

	public SshTransport(SshConfig config) {
		this.ssh = new SSHClient();
		
		try {
			this.ssh.loadKnownHosts();
		} catch (IOException e) {
			throw new RuntimeException(e);
		} 
	}

	@Override
	public FileInfo get(URI file) {
		Session session = null;
		try {
			session = getSession(file);
			final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
			final InMemoryDestFile destination = new InMemoryDestFile() {
				
				@Override
				public OutputStream getOutputStream() throws IOException {
					return outputStream;
				}
			};
			ssh.newSCPFileTransfer().download(file.getPath(), destination);
			
			final byte[] content = outputStream.toByteArray();
			final long length = content.length;
			final long lastModified = System.currentTimeMillis(); // TODO: not good
			return new FileInfo(length, lastModified, content);
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			close(session);
		}
	}

	@Override
	public void get(URI file, File destination) {
		Session session = null;
		try {
			session = getSession(file);
			ssh.newSCPFileTransfer().download(file.getPath(), destination.getAbsolutePath());
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			close(session);
		}
	}

	@Override
	public void getIfNewer(URI file, File destination) {
		// TODO: implements this if it's possible
		get(file, destination);
	}

	@Override
	public void put(File source, URI destination) {
		Session session = null;
		try {
			session = getSession(destination);
			ssh.newSCPFileTransfer().upload(source.getAbsolutePath(), destination.getPath());
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			close(session);
		}
	}

	@Override
	public List<URI> list(URI directory) {
		Session session = null;
		try {
			session = getSession(directory);
			final List<URI> uris = new ArrayList<URI>();
			final Command command = session.exec("ls -p1 --group-directories-first /" + directory.getPath());
			final String result = IOUtils.readFully(command.getInputStream()).toString();
			
			for (String line : result.split("[\n\r]+")) {
				if (line.endsWith("/")) {
					final String dir = line.substring(0, line.length() - 2);
					uris.add(new URI(directory + "/" + dir));
				}
			}
			return uris;
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			close(session);
		}
	}

	private void close(Session session) {
		if (session != null && session.isOpen()) {
			try {
				session.close();
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	@Override
	public void cleanup() {
		if (ssh.isConnected()) {
			try {
				ssh.close();
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}
	}
	
	private synchronized Session getSession(URI directory) {
		try {			
			if (!ssh.isConnected()) {
				final int port = directory.getPort() < 0 ? 22 : directory.getPort();
				final String hostname = directory.getHost();

				ssh.connect(hostname, port);
			}
			
			if (!ssh.isAuthenticated()) {
				final Connector connector = ConnectorFactory.getDefault().createConnector();
				final String username = (directory.getUserInfo() == null) ?  System.getProperty("user.name") : directory.getUserInfo();
				
				if (connector == null) {
					ssh.authPublickey(username);
					
					// TODO: find other ways to authenticate the user
					throw new UnsupportedOperationException("No SSH agent found");					
				} else {
					final AgentProxy proxy = new AgentProxy(connector);					
					ssh.auth(username, getAuthMethods(proxy));
				}			
			}

			// TODO: Check if the connection is against the correct server
			// if (ssh.getRemoteHostname())
			
			return ssh.startSession();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}


	private static List<AuthMethod> getAuthMethods(AgentProxy agent) throws BufferException  {
		Identity[] identities = agent.getIdentities();
		List<AuthMethod> result = new ArrayList<AuthMethod>();
		for (Identity identity : identities) {
			result.add(new AuthAgent(agent, identity));
		}
		return result;
	}	
}
